#!/usr/bin/python3

"""
Dispatch an email to the right mailbox
"""

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import sys
import re
import shutil
import os
import os.path
from email.parser import BytesHeaderParser
from email.utils import getaddresses

# TODO: once nm.debian.org is python3, move most of this code to process/ and
#       make it unit-tested

VERSION="0.2"

class umask_override:
    """
    Context manager that temporarily overrides the umask during its lifetime
    """
    def __init__(self, umask):
        self.new_umask = umask
        self.old_umask = None

    def __enter__(self):
        # Set umask
        self.old_umask = os.umask(self.new_umask)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Restore umask
        os.umask(self.old_umask)
        return False


def open_db(sqlite=False):
    """
    Connect to the NM database and return the db connection.

    Returns the db connection and a function used to process query strings
    before sending them to the database. It is a noop for postgresql and a
    replacement of "%s" with "?" on sqlite.

    That's what you get when some smart one decides to make a DB-independent
    db_api that supports only a DB-specific syntax for SQL query arguments.
    """
    if sqlite:
        import sqlite3
        db = sqlite3.connect("data/db-used-for-development.sqlite")
        return db, lambda s: s.replace("%s", "?").replace("true", "1")
    else:
        import psycopg2
        return psycopg2.connect("service=nm user=nm"), lambda x: x


class IncomingMessage:
    re_dest = re.compile("^archive-(?P<key>.+)@nm.debian.org$")

    def __init__(self, infd):
        self.infd = infd

        # Parse the header only, leave the body in the input pipe
        self.msg = BytesHeaderParser().parse(self.infd)

        # History of lookup attempts
        self.lookup_attempts = []

    def log_lookup(self, msg):
        self.lookup_attempts.append(msg)
        self.msg.add_header("NM-Archive-Lookup-History", msg)

    def log_exception(self, exc):
        self.msg.add_header("NM-Archive-Lookup-History", "exception: {}: {}".format(exc.__class__.__name__, str(exc)))

    def deliver_to_mailbox(self, pathname):
        with umask_override(0o037) as uo:
            with open(pathname, "ab") as out:
                out.write(self.msg.as_string(True).encode("utf-8"))
                out.write(b"\n")
                shutil.copyfileobj(self.infd, out)

    def get_dest_key(self):
        """
        Lookup the archive-(?P<key>.+) destination key in the Delivered-To mail
        header, extract the key and return it.

        Returns None if no parsable Delivered-To header is found.
        """
        dests = self.msg.get_all("Delivered-To")
        if dests is None:
            self.log_lookup("No Delivered-To header found")
            return None

        for dest in dests:
            if dest == "archive@nm.debian.org":
                self.log_lookup("ignoring {} as destination".format(dest))
                continue

            mo = self.re_dest.match(dest)
            if mo is None:
                self.log_lookup("delivered-to '{}' does not match any known format".format(dest))
                continue

            return mo.group("key")

        self.log_lookup("No valid Delivered-To headers found")
        return None


    def lookup_mailbox_filename(self, key, sqlite=False):
        db, Q = open_db(sqlite)

        cur = db.cursor()
        query = """
        SELECT pr.archive_key
            FROM person p
            JOIN process pr ON pr.person_id = p.id
            WHERE pr.is_active
        """

        if '=' in key:
            # Lookup email
            email = key.replace("=", "@")
            self.log_lookup("lookup by email '%s'" % email)
            cur.execute(Q(query + "AND p.email=%s"), (email,))
        else:
            # Lookup uid
            self.log_lookup("lookup by uid '%s'" % key)
            cur.execute(Q(query + "AND p.uid=%s"), (key,))

        basename = None
        for i, in cur:
            basename = i

        if basename is None:
            return None
        else:
            return basename + ".mbox"


def get_dest_pathname(msg, sqlite=False):
    """
    Return a couple (destdir, filename) with the default directory and mailbox
    file name where msg should be delivered
    """
    try:
        key = msg.get_dest_key()
        if key is None:
            # Key not found in the message
            return "/srv/nm.debian.org/mbox/", "archive-failsafe.mbox"
        elif key.isdigit():
            # New-style processes
            return "/srv/nm.debian.org/mbox/processes", "process-{}.mbox".format(key)
        else:
            # Old-style processes, need a DB lookup
            fname = msg.lookup_mailbox_filename(key, sqlite)
            if fname is None:
                msg.log_lookup("Key {} not found in the database".format(repr(key)))
                return "/srv/nm.debian.org/mbox/", "archive-failsafe.mbox"
            else:
                return "/srv/nm.debian.org/mbox/applicants", fname
    except Exception as e:
        msg.log_exception(e)
        return "/srv/nm.debian.org/mbox/", "archive-failsafe.mbox"


def main():
    import argparse
    parser = argparse.ArgumentParser(description="Dispatch NM mails Cc-ed to the archive address")
    parser.add_argument("--version", action="version", version="%(prog)s " + VERSION)
    parser.add_argument("--dest", action="store", default=None, help="override destination directory (default: hardcoded depending on archive address type)")
    parser.add_argument("--dry-run", action="store_true", help="print destinations instead of delivering mails")
    parser.add_argument("--sqlite", action="store_true", help="use the SQLite database on the development deployment instead of the production PostgreSQL")
    args = parser.parse_args()

    msg = IncomingMessage(sys.stdin.buffer)
    destdir, filename = get_dest_pathname(msg, args.sqlite)

    # Override destdir if requested
    if args.dest: destdir = args.dest

    # Deliver
    pathname = os.path.join(destdir, filename)
    if args.dry_run:
        for warn in msg.msg.get_all("NM-Archive-Lookup-History", []):
            print(warn)
        print("Delivering to mailbox", pathname)
    else:
        msg.deliver_to_mailbox(pathname)


if __name__ == "__main__":
    sys.exit(main())
